﻿using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace ShaderMachine
{
    public class ShaderManager
    {
        #region Declarations & Constructor
        private GraphicsDevice graphicsDevice;

        private List<Shader> effects;

        private RenderTarget2D finalRenderTarget;
        private RenderTarget2D unshadedRenderTarget;
        //private Texture2D screenTexture;
        private bool doneAnyShading;

        /// <summary>
        /// A class handling a list of shaders. Shaders have to be manually
        /// added, removed and altered from the effects list.
        /// The normal rendering should be done after DrawPreparation() and before
        /// DrawEffects(...).
        /// </summary>
        /// <param name="graphicsDevice"></param>
        public ShaderManager(GraphicsDevice graphicsDevice)
        {
            this.graphicsDevice = graphicsDevice;
            unshadedRenderTarget = CloneRenderTarget(graphicsDevice);
            //screenTexture = new Texture2D(graphicsDevice,
            //    unshadedRenderTarget.Width, unshadedRenderTarget.Height,
            //    false, unshadedRenderTarget.Format);
            effects = new List<Shader>();

        }
        #endregion //Declarations & Constructor

        #region Add, Set etc.
        public Shader AddShader(Effect effect, string name, string currentTechName, bool active)
        {
            Shader shader = new Shader(effect, name, effect.Techniques[currentTechName], active);
            effects.Add(shader);
            return shader;
        }

        public void SetShaderValue(string effectName, string parameterName, int value)
        {
            Shader s = effects.FirstOrDefault(f => f.name == effectName);
            if (s != null) s.effect.Parameters[parameterName].SetValue(value);
        }
        public void SetShaderValue(string effectName, string parameterName, float value)
        {
            Shader s = effects.FirstOrDefault(f => f.name == effectName);
            if (s != null) s.effect.Parameters[parameterName].SetValue(value);
        }
        public void SetShaderValue(string effectName, string parameterName, bool value)
        {
            Shader s = effects.FirstOrDefault(f => f.name == effectName);
            if (s != null) s.effect.Parameters[parameterName].SetValue(value);
        }

        public float GetShaderValue(string effectName, string parameterName)
        {
            Shader s = effects.FirstOrDefault(f => f.name == effectName);
            /*if (s != null)*/ return s.effect.Parameters[parameterName].GetValueSingle();
        }

        public void IncShaderValue(string effectName, string parameterName, float value)
        {
            Shader s = effects.FirstOrDefault(f => f.name == effectName);
            if (s != null) s.effect.Parameters[parameterName].SetValue(s.effect.Parameters[parameterName].GetValueSingle() + value);
        }
        public void IncShaderValue(string effectName, string parameterName, int value)
        {
            Shader s = effects.FirstOrDefault(f => f.name == effectName);
            if (s != null) s.effect.Parameters[parameterName].SetValue(s.effect.Parameters[parameterName].GetValueInt32() + value);
        }

        public Shader GetShader(string effectName)
        {
            return effects.FirstOrDefault(f => f.name == effectName);
        }

        public bool IsActive(string effectName)
        {
            return effects.Any(f => f.name == effectName && f.active);
        }

        public void ToggleShader(string effectName, bool active)
        {
            effects.ForEach(f => f.active = f.name == effectName ? active : f.active);
        }
        public void ToggleShader(string effectName)
        {
            effects.ForEach(f => f.active = f.name == effectName ? !f.active : f.active);
        }
        #endregion //Add, Set etc.

        #region Draw
        /// <summary>
        /// Performs some preparatory work, including clearing the GraphicsDevice.
        /// This method should be called before SpriteBatch.Begin().
        /// </summary>
        /// <param name="clearColor"></param>
        public void DrawPreparation(Color clearColor, RenderTarget2D renderTarget)
        {
            // Change to our offscreen render target.
            //if (graphicsDevice.GetRenderTargets().Length == 0)
            //    finalRenderTarget = null;
            //else
            //    finalRenderTarget = (RenderTarget2D)graphicsDevice.GetRenderTargets()[0].RenderTarget;
            finalRenderTarget = renderTarget;
            graphicsDevice.SetRenderTarget(unshadedRenderTarget);
            graphicsDevice.Clear(clearColor);
        }

        /// <summary>
        /// Applies the post-processing effects.
        /// This method should be called after SpriteBatch.End().
        /// </summary>
        /// <param name="spriteBatch"></param>
        public void DrawEffects(SpriteBatch spriteBatch, BlendState blendState, Color clearColor)
        {
            doneAnyShading = false;
            // Change back to the back buffer, and get our scene
            // as a texture.
            graphicsDevice.SetRenderTarget(finalRenderTarget);
            graphicsDevice.Clear(clearColor);
            //screenTexture = unshadedRenderTarget;

            // Use Immediate mode and our effect to draw the scene
            // again, using our pixel shader.
            spriteBatch.Begin(SpriteSortMode.Immediate, blendState);
            foreach (Shader shader in effects.Where(e => e.active))
            {
                //In case there are multiple shaders with the same effect, we set the current tech.
                shader.effect.CurrentTechnique = shader.tech;
                foreach (EffectPass pass in shader.effect.CurrentTechnique.Passes)
                {
                    pass.Apply();
                    spriteBatch.Draw(unshadedRenderTarget, Vector2.Zero, clearColor);
                    doneAnyShading = true;
                }
            }
            if(!doneAnyShading)
                spriteBatch.Draw(unshadedRenderTarget, Vector2.Zero, clearColor);
            spriteBatch.End();
        }

        /// <summary>
        /// Applies the post-processing effects of just a single shader.
        /// This method should be called after SpriteBatch.End().
        /// </summary>
        /// <param name="spriteBatch"></param>
        /// <param name="shaderName"></param>
        public void DrawEffect(SpriteBatch spriteBatch, Shader shader, BlendState blendState, Color clearColor)
        {
            doneAnyShading = false;
            graphicsDevice.SetRenderTarget(finalRenderTarget);
            graphicsDevice.Clear(clearColor);

            spriteBatch.Begin(SpriteSortMode.Immediate, blendState);
            //In case there are multiple shaders with the same effect, we set the current tech.
            shader.effect.CurrentTechnique = shader.tech;
            foreach (EffectPass pass in shader.effect.CurrentTechnique.Passes)
            {
                pass.Apply();
                spriteBatch.Draw(unshadedRenderTarget, Vector2.Zero, clearColor);
            }
            if (!doneAnyShading)
                spriteBatch.Draw(unshadedRenderTarget, Vector2.Zero, clearColor);
            spriteBatch.End();
        }

        public void DrawEffect(SpriteBatch spriteBatch, string shaderName, BlendState blendState, Color clearColor)
        { DrawEffect(spriteBatch, effects.FirstOrDefault(e => e.name == shaderName), blendState, clearColor); }
        #endregion //Draw

        /// <summary>
        /// Creates a new RenderTarget, based on the settings of the GraphicsDevice.
        /// </summary>
        /// <param name="device"></param>
        /// <returns></returns>
        private RenderTarget2D CloneRenderTarget(GraphicsDevice device)
        {
            return new RenderTarget2D(device,
                device.PresentationParameters.BackBufferWidth,
                device.PresentationParameters.BackBufferHeight,
                false,
                device.DisplayMode.Format,
                DepthFormat.Depth24Stencil8);
        }
    }
}